program ChromeSvnTrace;

include <core.vhs>
include <shell.vhs>
include "chrome.vhs"

import core::*;
import core::Pattern::*;
import core::Logging::*;
import shell::*;

function main(maxentry='2000000') {
	Logging::configureLog("svntrace2.log");
	log("Start processing Svn Trace");
	//var outfile = new CsvFile('output/rev-file-changelines.csv');
	//var vulnlineFile = new CsvFile('output/rev-file-vulnlines.csv');
	var outfile = new CsvFile('output/bug-changelines.csv');
	outfile.writeln('Bug,VulnRevision,OrgRevision,Line,FSF,LoC');
	
	RevisionLine(input = new SequenceStream('output/bugfile.csv', 4096), maxOccur=Runtime.asInt(maxentry),
			outfunc = function(data) {
				outfile.writeCsvLine([data.bug, data.revision, data.orgrevision, data.line, data.path, data.loc]);
			});
	outfile.close();
	//vulnlineFile.close();
	log("Completed.");
}

function resolveFullPath(path) {
	var fullpath = '';
	for (var i = chrome::CODEBASES_COUNT - 1; i >= 0; i--) {
		fullpath = chrome::getSrcLocation(i) + path;
		if (fileExists(fullpath)) return fullpath;
	}
	return null;
}

pattern<DataPart> RevisionLine($dataPattern=@'^(\d+),(\d+),/trunk/src/(.+?\.(c|cc|cpp|cxx|h|hh|hpp|hxx))$', $flag = MULTILINES, $outfunc) {
	var bug = this[1] + '';
	var rev = this[2] + '';	
	var path = this[3] + '';
	var rev_1 = Runtime.asInt(rev) - 1;
	var fullpath = resolveFullPath(path);
	var cmd = format('/usr/bin/svn diff -x "-u -b -w" -c %s %s', rev, fullpath);
	
	log("\n-------------\nProcess bug: %s, file %s [rev: %s]", bug, path, rev);	
	var isTest = fullpath?.indexOf('tests') > 0 ||
				fullpath?.indexOf('unittest') > 0;
	if (fullpath != null && !isTest) {	
		var diff_file = 'diff-cache/' + path.replace('/', '_') + '.r' + rev + '.diff';	
		var code = if (fileExists(diff_file)) 0 
					else Runtime.exec2(diff_file, ['/bin/sh', '-c', cmd]);
		if (code == 0) {
			var changeLines = 0;
			UnifiedDiff(input = new SequenceStream(diff_file, 4096), 
				outfunc = function(line, loc) { 
					//changeLines = changeLines + lines; 
					var blame = getLineRevision(path, rev_1, line);
					outfunc?(struct { bug=bug, revision=rev_1, orgrevision=blame, path=path, line=line, loc=loc });
					changeLines++;
				} );
			if (changeLines == 0) {  
				// change is only to add new code
				// output -1 as changeline index
				outfunc?(struct { bug=bug, orgrevision=-1, revision=rev_1, path=path, line='-1', loc='' });
			}
		}
		else
			log_warning("Error running diff on file %s [rev:%s]: error code = %d", fullpath, rev, code);
	}
	else if (fullpath == null) log_warning("%s not found in any codebase", path);
	else log_warning("Skip [%s], since it is a test file", path)
}

pattern UnifiedDiff($outfunc, $minOccur=0, $maxOccur=INFINITY) {
	var header = locate(@'@@ -(\d+),(\d+) \+\d+,\d+ @@$');
	locate_end(@'@@ -(\d+),(\d+) \+\d+,\d+ @@', excludeMatchedText=true, lastPosOnUnmatch=true);
	
	var totalLine = Runtime.asInt(header[2]);
	var startLine = Runtime.asInt(header[1]);
	//var changeLines = '';
	var ln;
	var c;
	var trivialChange;
	var loc;
	var action;
	
	println('Header: %d, %d', startLine, totalLine);
	for(var line = 1; line <= totalLine; ) {
		ln = ReadLine();
		c = ln.charAt(0);
		action='[ ]';
		trivialChange = false;
		if (c == '-'.charAt(0)) {
			// TrivivalChange detector checks whehther change is located in a line comment.
			// Such change makes no sense in code
			loc = ln.substring(1).trim();
			trivialChange = loc == '' || loc.startsWith("//") || loc == '}';
			if (!trivialChange) {
				//changeLines = changeLines + (line + startLine - 1) + ';';
				//lines?.add(line + startLine - 1);
				outfunc?(line + startLine - 1, loc);
				action = '[x]';
			}
			else {
				log("Trivial change detected at line %d", line + startLine - 1);
				action = '[*]';
			}
			
		}
		println("%d: %s%s", line, action, ln);
		if (c != '+'.charAt(0)) {			
			line++;
		}		
	}
	//outfunc?(changeLines);
}

pattern<DataPart> ReadLine($dataPattern=@'^((\+|-| )[^\n]*)\n') this[1];

function getLineRevision(path, revision, line) {
	var blame_file = chrome::blameRevision(path, revision);
	if (blame_file != null) {
		var ip = new SequenceStream(blame_file, 4096);
		var res = chrome::AnnotatedLine(input = ip, minOccur=line, maxOccur=line);
		ip.close();
		return res[0];
	}
	return null;
}

pattern SvnBlame($lines, $revs) {
	var line;
	var lineIdx = 0;
	var totalLine = lines.size();
	for(var i = 0; i < totalLine; ) {
		line = AnnotatedLine();
		lineIdx++;
		if (lineIdx == lines[i]) {
			//println("[%s] %d", line, lines[i]);
			if (revs.containsKey(line))
				revs.put(line, revs[line] + lines[i] + ';');
			else
				revs.put(line, lines[i].toString() + ';');
			i++;
		}
	}
}